<?php
namespace DRY\Module;

/**
 * DonReY Framework 2012 :: News/Blog like CMS
 * Most suited for project which present information ( articles ) ordered normally by time ( like a news site or a blog system )
 * Features -(planned) !(done) ~(partial):
 * - Time ordered articles containing title, contents, file attachments, list of labels, and comment list
 * - Editing articles using text-only syntax ( no rich-text, to avoid messy HTML ), using Textile syntax ( + extensions )
 * ~ Search article by url_fragment, date, title, contents
 * ~ Retrieve articles from a certain time period ( last N days, last N months, arbitrary month, arbitrary year )
 * ! Article categories, non-hierarchic (to organize articles)
 * - List of all used labels which can generate a label cloud (or tag cloud)
 * - File attachments to article ( can be image/multimedia uploads which can be used inside the article )
 * ! User comments with recording of users Email, Website address, counting comments for same user
 * ~ Supports paging / result limiting on all types of lists
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Structure/Modules/cmsnews.html
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version 1.0.dev
 * @package DonReY
 */
class News extends base
{
	// Exception/Error codes
	const
		EXC_ARTICLES_FEATURE_MISSING = 1,
		EXC_FEATURE_NOT_ENABLED = 2
		;

	/**
	 * Module Constructor
	 * @param string $alias
	 * @param SimpleXMLElement $config
	 */
	public function __construct($alias, $config)
	{
		parent::__construct($alias, $config);

		// make some table definitions | NOTE: Afert Record module will support data type translation, "mode" might be changed to something else
		if(isset($this-> CFG-> features-> articles)) {
			$this['record']-> define_Record('article', array(
				'type'=>'single',
				'table'=>(string)$this-> CFG-> features-> articles['table'],
				'fields'=>array('timedate'=>true, 'user_id'=>true, 'category_id'=>true, 'title'=>true, 'uri_frag'=>true, 'contents'=>true)
			));
		} else {
			throw new \DRY\Water('news', self::EXC_ARTICLES_FEATURE_MISSING);
		}

		if(isset($this-> CFG-> features-> categories)) {
			$this['record']-> define_Record('category', array(
				'type'=>'single',
				'table'=>(string)$this-> CFG-> features-> categories['table'],
				'fields'=>array('category'=>true, 'uri_frag'=>true, 'articles_count'=>true)
			));
		}

		if(isset($this-> CFG-> features-> comments)) {
			$this['record']-> define_Record('commentator', array(
				'type'=>'single',
				'table'=>(string)$this-> CFG-> features-> comments-> users['table'],
				'fields'=>array('email'=>true, 'name'=>true, 'website'=>true, 'count_comments'=>true)
			));
			$this['record']-> define_Record('comment', array(
				'type'=>'single',
				'table'=>(string)$this-> CFG-> features-> comments['table'],
				'fields'=>array('article_id'=>true, 'commentator_id'=>true, 'timedate'=>true, 'comment'=>true)
			));
		}

		if(isset($this-> CFG-> features-> attachments)) {
			$this['record']-> define_Record('attach', array(
				'type'=>'single',
				'table'=>(string)$this-> CFG-> features-> attachments['table'],
				'fields'=>array('type'=>true, 'uri_frag'=>true, 'chash'=>true, 'ext'=>true, 'filesize'=>true, 'name'=>true, 'description'=>true, 'count_refs'=>true, 'timedate'=>true)
			));
		}
	}

	/**
	 * Creates an indentifier usable in an URL by converting some characters in the $input
	 * (Work in progress, to convert â to a for example, or ß to ss)
	 * Can also search if the identifier is already used in some table(s), and appends a number to make unique
	 *
	 * @param string $input
	 * @param string $db_table (Optional)
	 * @param string $db_field (Optional)
	 */
	public function uri_frag_Generate($input, $db_search_table = null, $db_field = 'uri_frag')
	{
		$frag = (string)$input;

		// for now, replaces anything non-alphanumeric with underscores
		$frag = strtolower(trim(preg_replace("~([\\s_]|[^\\w])+~s", '_', $frag),'_'));
		// TODO: The database search part
		return $frag;
	}

//----- Articles -----//

	/**
	 * Creates a new article in the news system
	 * @param string $title Article title
	 * @param string $contents Artice contents ( might be long )
	 * @param id $user_id ID of the user who posted this articles ( or 0 if there's only one user, the site administrator )
	 * @param array $extra Extra information ( depends on enabled features - might contain category, labels, attachment references )
	 */
	public function article_Post($title, $contents, $user_id = 0, $extra = array())
	{
		$art = $this['record']-> factory_Record('article');
		$art-> timedate = date("Y-m-d H:i:s");
		$art-> user_id = $user_id;
		$art-> title = htmlspecialchars($title);
		$art-> contents = htmlspecialchars($contents, ENT_NOQUOTES);
		$art-> uri_frag = $this-> uri_frag_Generate(isset($extra['uri_frag'])? $extra['uri_frag']: $title);

		// feature: Categories
		if(isset($this-> CFG-> features-> categories)) {
			$art-> category_id = isset($extra['category_id'])?
				$extra['category_id']:
				(isset($this-> CFG-> features-> categories-> default)? (int)$this-> CFG-> features-> categories-> default['id'] : 0);
			// check if category with id actually exists
			$cat = $this-> category_Read($art-> category_id);
			if(!isset($cat-> category))
				$art-> category_id = 0; // fail
			else
				// and update it's article count
				$cat-> articles_count = $cat-> articles_count + 1;
		}
		return $art-> id;
	}

	/**
	 * Updates an existing article (can change anything: Title, Contents, User, Category, Labels, Attachments - except "timedate")
	 * @param int $id
	 * @param array $update_data
	 * @return ORM_Record_direct The modified article data (might need to send some of it to JavaScript)
	 */
	public function article_Update($id, $update_data = array())
	{
		$art = $this['record']-> factory_Record('article', $id);
		if(isset($update_data['user_id']))
			$art-> user_id = $update_data['user_id'];
		if(isset($update_data['title']))
			$art-> title = htmlspecialchars($update_data['title']);
		if(isset($update_data['contents']))
			$art-> contents = htmlspecialchars($update_data['contents'], ENT_NOQUOTES);
		// uri_frag is filtered through uri_frag_generate
		if(isset($update_data['uri_frag'])) {
			if(is_string($update_data['uri_frag']))
				$art-> uri_frag = $this-> uri_frag_Generate($update_data['uri_frag']);
			elseif($update_data['uri_frag'] === true)
				$art-> uri_frag = $this-> uri_frag_Generate($update_data['title']);
		}

		// feature: Categories
		if(isset($update_data['category_id'])) {
			// check if the new category with id actually exists, only updates if it does
			$cat = $this-> category_Read($update_data['category_id']);
			if(isset($cat-> category))
				$art-> category_id = $update_data['category_id'];
		}
		return $art;
	}

	/**
	 * Reads an article and all it's associated data ( User, Category, Labels, Attachments )
	 * @param int $id
	 * @param string $where Allows reading the article by random criteria (instead of ID, in this case, $id must be 0 and $where_callback a valid callback
	 * @param bool $features False = Disables all features reading (usually for editing)
	 * @return array Note that this doesn't return a Record object, since an article is more complex and currently Record objects do not support multiple tables and complex data
	 */
	public function article_Read($id, $where_callback = null, $features = true)
	{
		$q = $this['queryfactory']->select()
			->table(array((string)$this-> CFG-> features-> articles['table'], 'art'))
			->limit(1);
		if($id > 0)
			$q-> where('art.id = ?', $id);
		else
			$where_callback($q, 'art');

		$res = $this-> article_JoinTables_RunQuery($q, $features);
		$article = $res-> current();

		if($features) {
			// feature: Comments
			if(isset($this-> CFG-> features-> comments)) {
				$comments = $this-> comment_Read4Article($article['id']);
				foreach($comments as $comment)
					$article['comments'][$comment['id']] = $comment;
			}
		}
		return $article;
	}

	private function article_JoinTables_RunQuery($q, $features = true)
	{
		$split_by_prefixes = array();

		if($features) {
			// feature: Users
			if(isset($this-> CFG-> features-> articles-> users)) {
				$q-> table(array((string)$this-> CFG-> features-> articles-> users['table'], 'usr'), "{$this-> CFG-> features->articles-> users['display']}:usr_display,{$this-> CFG-> features-> articles-> users['link']}:usr_link", "left", "art.user_id = usr.id");
				$split_by_prefixes['user']='usr_';
			}

			// feature: Categories
			if(isset($this-> CFG-> features-> categories)) {
				$q-> table(array((string)$this-> CFG-> features-> categories['table'], 'cat'), "category:cat_name,uri_frag:cat_uri_frag", "left", "art.category_id = cat.id");
				$split_by_prefixes['category']='cat_';
			}
		}

		// run query and return first result (should be only one since selet is by ID)
		$res = $this['connection']->query($q);
		$res-> split_by_prefixes = $split_by_prefixes;
		return $res;
	}

	/**
	 * Search an article by it's URI frag and returns all associated data - @see article_Read
	 * @param string $uri_frag
	 * @return array
	 */
	public function article_UriSearch($uri_frag, $features = true)
	{
		return $this-> article_Read(0, function($q, $table_alias) use ($uri_frag) {
			$q-> where("{$table_alias}.uri_frag = ?", $uri_frag);
		}, $features);
	}

	/**
	 * Reads multiple articles based on various criterias
	 * @return DB_Driver_result_base
	 */
	public function article_ReadMultiple($mode = "news", $criteria = null, $count = 10, $startwith = 0)
	{
		$q = $this['queryfactory']->select()
			-> table(array((string)$this-> CFG-> features-> articles['table'], 'art'))
			-> order('timedate', 'DESC')
			-> limit($count, $startwith);
		switch($mode) {
			case 'news':
				// nothing else here
				break;
			case 'category':
				$q-> where('category_id = ?', $criteria);
				break;
			case 'month': // [0 = year, 1 = month]
				$q-> where('YEAR(timedate) = ? && MONTH(timedate) = ?', $criteria[0], $criteria[1]);
				break;
		}
		$res = $this-> article_JoinTables_RunQuery($q);
		return $res;
	}

	/**
	 * Removes an article and all it's associated data
	 * @param int $id
	 */
	public function article_Delete($id)
	{
		// first read the article to find other information about it ( category, user, ? )
		$art = $this['record']-> factory_Record('article', $id);
		if(!isset($art-> title))
			 return false; // article not found

		// feature: Categories
		if($art-> category_id > 0) {
			$cat = $this-> category_Read($art-> category_id);
			// update categoriy's article count ( if it exists )
			if(isset($cat-> category))
				$cat-> articles_count = $cat-> articles_count - 1;
		}

		// feature: Comments
		if(isset($this-> CFG-> features-> comments)) {
			// quick-read comments on article, to discover who commented them and how much
			$q = $this['queryfactory']-> select()
				-> table(array((string)$this-> CFG-> features-> comments['table'], 'com'), 'commentator_id,COUNT(#.commentator_id):count_comments')
				-> where('article_id = ?', $id)
				-> group('commentator_id');
			$res = $this['connection']-> query($q);
			// update commentators by reducing their comment count
			foreach($res as $r) {
				$this['connection']-> query($this['queryfactory']-> update()
					-> table((string)$this-> CFG-> features-> comments-> users['table'])
					-> values(array("count_comments"=>"`count_comments` - {$r['count_comments']}"), null, true)
					-> where("id = ?", $r['commentator_id'])
				);
			}
			// then delete all comments linked to this article
			$this['connection']-> query($this['queryfactory']-> delete()
				-> table((string)$this-> CFG-> features-> comments['table'])
				-> where('article_id = ?', $id)
			);
			// and finally remove the article itself
			$art-> destroy();
		}
	}

//----- Feature: Article Categories ( 1 to M ) -----//

	/**
	 * Creates or updates a category
	 * @param int $id
	 * @param string $category Name of this category
	 * @param string $uri_frag URI fragment | False = keep current category name, True = (re)generate from $category, String = $uri_frag
	 * @return int Category's ID ( new or existing )
 	 * @throws Exception
	 */
	public function category_Write() // $id = 0, $category = '', $uri_frag = false
	{
		if(!isset($this-> CFG-> features-> categories))
			throw new \DRY\Water('news', self::EXC_FEATURE_NOT_ENABLED, array('feature'=> 'categories'));

		$a = func_get_args();
		if(is_int($a[0])) {
			if(count($a)==2) $a[2] = false;
		} else {
			if(count($a)==1) $a[1] = false;
			array_unshift($a, 0);
		}
		list($id, $category, $uri_frag) = $a;

		$cat = $this['record']-> factory_Record('category', $id);
		if(!empty($category))
			$cat-> category = $category;
		if(is_string($uri_frag))
			$cat-> uri_frag = $this-> uri_frag_Generate($uri_frag);
		elseif($uri_frag === true || $id == 0)
			$cat-> uri_frag = $this-> uri_frag_Generate($category);
		return $cat-> id;
	}

	/**
	 * Returns a category object (Record) by specifying it's ID - Note that this object can be used for updating/removing, instead of the _Write and _Delete methods
	 * @param string $id
	 * @return ORM_Record_direct The category data
	 * @throws Exception
	 */
	public function &category_Read($id)
	{
		if(!isset($this-> CFG-> features-> categories))
			throw new \DRY\Water('news', self::EXC_FEATURE_NOT_ENABLED, array('feature'=> 'categories'));

		$cat = $this['record']-> factory_Record('category', $id);
		return $cat;
	}

	/**
	 * Search categories and return a category object (Record) by it's URI frag - @see category_Read
	 * @param string $uri_frag
	 * @return ORM_Record_direct The category data
	 */
	public function &category_UriSearch($uri_frag)
	{
		if(!isset($this-> CFG-> features-> categories))
			throw new \DRY\Water('news', self::EXC_FEATURE_NOT_ENABLED, array('feature'=> 'categories'));

		$cat = $this['record']-> factory_Record('category');
		$cat-> read(true, function($qf) use($uri_frag) {
			$qf-> where('uri_frag = ?', $uri_frag);
		});
		return $cat;
	}

	/**
	 * Removes a blog category
	 * @param int $id
	 * @throws Exception
	 */
	public function category_Delete($id)
	{
		if(!isset($this-> CFG-> features-> categories))
			throw new \DRY\Water('news', self::EXC_FEATURE_NOT_ENABLED, array('feature'=> 'categories'));

		$cat = $this['record']-> factory_Record('category', $id);
		return $cat-> destroy();
	}

//----- Feature: Article Comments and Commentators (Users) ( 1 to M ) -----//

	/**
	 * Checks if a commentator user exists (by searching it's e-mail), and possibly updates it's stored name and website, or creates it if not found
	 * A commentator has e-mail, username (optionally website url) stored, but doesn't necesarely have an access account on the website
	 * @param string $email
	 * @param string $name Must exist if the e-mail is new, or the function will use e-mail as name, but replacing @ with " [at] "
	 * @param string $website
	 * @return ORM_Record_direct The commentator object
	 */
	public function commentator_Check($email, $name = null, $website = null)
	{
		if(!isset($this-> CFG-> features-> comments))
			throw new \DRY\Water('news', self::EXC_FEATURE_NOT_ENABLED, array('feature'=> 'comments'));

		if(filter_var($email, FILTER_VALIDATE_EMAIL)===false)
			return false; // error, invalid e-mail

		$comt = $this['record']-> factory_Record('commentator');
		$comt-> read(true, function($q) use ($email) {
			$q-> where('email = ?', $email);
		});
		// TODO: sanitize (and check if it works) name and website, to prevent html or javascript injection
		$name = filter_var($name, FILTER_SANITIZE_STRING);
		if(!isset($comt-> email)) {
			// not found, create
			$comt-> email = $email;
			if(empty($name))
				$comt-> name = str_replace('@', ' [at] ', $email);
			else
				$comt-> name = $name;
		} else {
			if(!empty($name))
				$comt-> name = $name;
		}
		if(!is_null($website)) {
			// not that validation kinda sucks, allowing some weirdo URL's to pass-through
			$try1 = filter_var($website, FILTER_VALIDATE_URL);
			if($try1 === false) // attempt to attach http:// and revalidate (since most people don't put that by default)
				$website = 'http://'.$website;
			$comt-> website = filter_var($website, FILTER_VALIDATE_URL);
		}
		return $comt;
	}

	/**
	 * Creates a comment and appends it to an existing article
	 * @param int $commentator Obtain a commentator object by using the commentator_Check function first
	 * @param int $article_id
	 * @param string $comment
	 */
	public function comment_Append2Article($commentator, $article_id, $comment)
	{
		if(!isset($this-> CFG-> features-> comments))
			throw new \DRY\Water('news', self::EXC_FEATURE_NOT_ENABLED, array('feature'=> 'comments'));

		// create the comment
		$com = $this['record']-> factory_Record('comment');
		$com-> commentator_id = $commentator-> id;
		$com-> article_id = $article_id;
		$com-> timedate = date("Y-m-d H:i:s");
		$com-> comment = filter_var($comment, FILTER_SANITIZE_STRING);

		// and update number of comments for the commentator
		$commentator-> count_comments = $commentator-> count_comments + 1;
	}

	/**
	 * Reads all comments for an article, in normal order ( top to bottom ), along with commentator's data
	 * @param int $article_id
	 * @return DB_Driver_result_base
	 */
	public function comment_Read4Article($article_id)
	{
		if(!isset($this-> CFG-> features-> comments))
			throw new \DRY\Water('news', self::EXC_FEATURE_NOT_ENABLED, array('feature'=> 'comments'));

		$q = $this['queryfactory']->select()
			-> table(array((string)$this-> CFG-> features-> comments['table'], 'com'), "id,comment,timedate")
			-> where('article_id = ?', $article_id)
			-> order('com.id', 'ASC')
			-> table(array((string)$this-> CFG-> features-> comments-> users['table'], 'comt'), "email,name,website", 'left', 'comt.id = com.commentator_id');

		$res = $this['connection']-> query($q);
		return $res;
	}

	/**
	 * Removes a comment
	 * @param int $id
	 */
	public function comment_Delete($id)
	{
		// read the comment to see the commentator id
		$com = $this['record']-> factory_Record('comment');
		if(isset($com-> commentator_id)) {
			// update the commentator's count_comments
			$this['connection']-> query($this['queryfactory']-> update()
				-> table((string)$this-> CFG-> features-> comments-> users['table'])
				-> values(array("count_comments"=>"`count_comments` - 1"), null, true)
				-> where("id = ?", $com-> commentator_id)
			);
		}
		// then remove the comment itself
		$com-> destroy();
	}

//----- Feature: Attachments (Files) ( M to M ) -----//

	/**
	 * Takes a file (usually from an upload), determines it's type, hashes it's content and places it into a folder.
	 * Also creates the database record which relates to that file, and returns it
	 *
	 * @param string $current_path
	 * @param string $name
	 * @param string $description
	 * @param array $extra
	 */
	public function attachment_Analyse($current_path, $name, $description = null, $extra = array(), $source = "POST")
	{
		$finfo = new \SplFileInfo($current_path);

		$filename = $finfo->getFilename();
		$filedata=array(
			'type'=>$finfo->getType(),
			'name'=>$filename,
			'ext'=>$fileext = (($p=strrpos($filename,'.'))!==false)?substr($filename,$p+1):'', // might be empty if strrpos doesn't find a dot,
//			'ctime'=>$finfo->getCTime(),
//			'mtime'=>$finfo->getMTime(),
			'size'=>$finfo->getSize(),
			'chash'=>sha1_file($current_path)
		);

		$sub1=$filedata['chash'][0];
		$sub2=$filedata['chash'][1];

		$storage_rel_path = constant(isset($this-> CFG-> features-> attachments-> storage['rel'])?(string)$this-> CFG-> features-> attachments-> storage['rel']:'APP_PATH');
		$path = realpath($storage_rel_path.$this-> CFG-> features-> attachments-> storage).'/'.$sub1.'/'.$sub2;

		$file_path = $path. '/'. $filedata['chash'].'.'.strtolower($filedata['ext']);

		// same file with same contents was already uploaded before, reused it
		if(file_exists($file_path)) {

		} else {
			// path doesn't exist yet, create it before moving the file
			if(!is_dir($path)) {
				mkdir($path, 0777, true);
			}

			if($source == 'POST')
				move_uploaded_file($current_path, $file_path);
			else
				rename($current_path, $file_path);
		}

		$att = $this['record']-> factory_Record('attach');

		// - Attempt to analyse files of certain extensions as image (jpg, png, gif) - only types natively supported by PHP
		if(in_array($filedata['ext'], array('jpg', 'jpeg', 'png', 'gif'))) {
			// TODO: Module imager (it actually exists, just needs updating for DRY2012)
			$filetype_image = &$this-> CFG-> features-> attachments-> types-> image;
//			$imager = \DRY\core::m((string)$filetype_image['imager']);
//			$img_data = $imager-> analyse_File($file_path);
//			if($img_data!==false) {
//				$img_data['th_width'] = $extra['thwidth']?:$filetype_image['thdefwidth'];
//				$img_data['th_height'] = $extra['thheight']?:$filetype_image['thdefheight'];
//				$img_data['thm'] = $imager-> generate_ThumnailFromFile($file_path, $img_data['th_width'], $img_data['th_height']);
//				$img = $this['record']-> factory_Record('file_image');
//				$img-> import($img_data);
//				$att-> type = 'image';
//			} else {
				// failed to analyse as image, type will be just "general"
				$att-> type = 'general';

//			}
		} else { // defaults to "general file", which will appear as a download link ( not embeddable )
			$att-> type = 'general';
		}
		// the rest of it
		$att-> uri_frag = $this-> uri_frag_Generate(isset($extra['uri_frag'])? $extra['uri_frag']: $name);
		$att-> chash = $filedata['chash'];
		$att-> ext = $filedata['ext'];
		$att-> filesize = $filedata['size'];
		$att-> name = $name;
		$att-> description = $description;
		$att-> timedate = date("Y-m-d H:i:s");
	}

	/**
	 * Connects an attachment to an article, allowing it to be used inside the article's text ( might be a link to a file, or an image which is visible, or something else )
	 * @param int $article_id
	 * @param int $attachment_id
	 */
	public function attachment_Append2Article($article_id, $attachment_id)
	{

	}

	/**
	 * Streams the content of the attachment file to the browser
	 * - depending on it's type, sets the MIME type, or picks a variant ( for example, an image can have a thumbnail )
	 * @param int $id
	 * @param array $params Parameters specific to the various attachment types
	 */
	public function attachment_Stream($id, $params = array())
	{
		$att = $this['record']-> factory_Record('attach', $id);

		$storage_rel_path = constant(isset($this-> CFG-> features-> attachments-> storage['rel'])?(string)$this-> CFG-> features-> attachments-> storage['rel']:'APP_PATH');
		$sub1=$att-> chash[0];
		$sub2=$att-> chash[1];
		$path = realpath($storage_rel_path.$this-> CFG-> features-> attachments-> storage).'/'.$sub1.'/'.$sub2;

		$file_path = $path. '/'. $att-> chash.'.'.strtolower($att-> ext);

//    header("Pragma: public"); // required
//    header("Expires: 0");
//    header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
//    header("Cache-Control: private",false); // required for certain browsers
//    header("Content-Type: $ctype");
//    header("Content-Disposition: attachment; filename=\"".basename($fullPath)."\";" );
//    header("Content-Transfer-Encoding: binary");

	// temp
    switch ($att-> ext) {
      case "pdf": $ctype="application/pdf"; break;
      case "exe": $ctype="application/octet-stream"; break;
      case "zip": $ctype="application/zip"; break;
      case "doc": $ctype="application/msword"; break;
      case "xls": $ctype="application/vnd.ms-excel"; break;
      case "ppt": $ctype="application/vnd.ms-powerpoint"; break;
      case "gif": $ctype="image/gif"; break;
      case "png": $ctype="image/png"; break;
      case "jpeg":
      case "jpg": $ctype="image/jpg"; break;
      default: $ctype="application/force-download";
    }

    	header("Content-Type: $ctype");
//    	header("Content-Disposition: attachment; filename=\"{$att-> name}.{$att-> ext}\";" );
    	header("Content-Length: {$att-> filesize}");
		readfile($file_path);
		die;
	}

//----- Feature: Labels ( M to M ) -----//

}